package controleur;

import java.awt.Dimension;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map.Entry;

import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.JProgressBar;
import javax.swing.SwingWorker;
import javax.swing.filechooser.FileFilter;

import logique.administration.Administration;
import logique.administration.Modification;
import logique.calculateur.Calculateur;
import logique.calculateur.Itineraire;
import logique.parser.Parser;
import logique.preferences.Critere;
import logique.preferences.CritereEtapes;
import logique.preferences.CritereItineraire;
import logique.preferences.CritereTroncon;
import logique.preferences.CritereVille;
import logique.preferences.PreferencesUtilisateur;
import logique.reseau.Reseau;
import logique.reseau.Route;
import logique.reseau.Troncon;
import logique.reseau.Ville;
import vues.InterfacePrincipale;

/**
 * Le {@link Controleur} a pour role de faire le lien entre les différents
 * modules de l'application. Il propose pour cela un ensemble d'accesseurs, de
 * modifieurs et de fonctions.
 * 
 * @author Camille
 * 
 */
public class Controleur {

	public static final String PROJ_NAME = "roadguide";
	public static final String RES_DIR = "reseaux";

	/**
	 * Lien vers l' {@link InterfacePrincipale} du programme
	 */
	private InterfacePrincipale interfacePrincipale;

	/**
	 * Lien vers le {@link Parser}
	 */
	private Parser parser;

	/**
	 * Lien vers le {@link Reseau}
	 */
	private Reseau reseau;

	/**
	 * Lien vers le module d' {@link Administration}
	 */
	private Administration administration;
	
	/**
	 * {@link Boolean} utilise pour le verrouillage des mises a jour (pendant la navigation notamment)
	 */
	private boolean lockUpdates;

	/**
	 * Lien vers les {@link PreferencesUtilisateur}
	 */
	private PreferencesUtilisateur preferences;

	/**
	 * Lien vers le module {@link Calculateur}
	 */
	private Calculateur calculateur;

	/**
	 * Chemin vers le dossier de travail du programme (par defaut "user.dir")
	 */
	private String workspace;

	/**
	 * Constructeur par defaut du {@link Controleur}. Initialise tous les
	 * liens.
	 */
	public Controleur() {
		initWorkspace();
		this.parser = new Parser(this);
		this.reseau = null;
		this.administration = new Administration(this);
		this.preferences = new PreferencesUtilisateur();
		this.calculateur = new Calculateur(this);
		this.interfacePrincipale = new InterfacePrincipale(this);
	}

	public void initWorkspace() {
		this.workspace = System.getProperty("user.dir") + File.separator + PROJ_NAME;
		File wDir = new File(this.workspace);

		if (!(wDir.exists() && wDir.isDirectory())) {
			wDir.mkdir();
		}

		File rDir = new File(this.workspace + File.separator + RES_DIR);
		if (!(rDir.exists() && rDir.isDirectory())) {
			rDir.mkdir();
		}
	}

	public Parser getParser() {
		return parser;
	}

	public void setParser(Parser parser) {
		this.parser = parser;
	}

	/**
	 * Indicateur de parsing complet
	 * 
	 * @return <code>true</code> si le reseau a ete entierment charge en
	 *         memoire, <code>false</code> sinon
	 */
	public boolean fullParsing() {
		return parser.fullParsing();
	}

	public String getWorkspace() {
		return workspace;
	}

	/**
	 * Cree et retourne la liste des fichiers decrivant des reseaux contenu dans
	 * le workspace
	 * 
	 * @return une {@link LinkedList} de {@link String} contenant les chemins
	 *         vers les fichiers descriptifs de reseaux
	 */
	public LinkedList<String> reseauxDisponibles() {
		File rDir = new File(workspace + File.separator + RES_DIR);
		LinkedList<String> pathList = new LinkedList<String>();

		for (File file : rDir.listFiles()) {
			if (file.getName().endsWith(Parser.FILE_EXT)) {
				pathList.add(file.getPath());
			}
		}

		return pathList;
	}

	/**
	 * Lance le calcul de l'itineraire (appelle
	 * <code>calculateur.calculeItineraire();</code>)
	 * 
	 * @return L' {@link Itineraire} calcule
	 */
	public Itineraire calculeItineraire() {
		calculateur.calculeItineraire(this.preferences);
		return calculateur.getItineraire();
	}

	public File importReseau() throws Exception {
		JFileChooser fC = new JFileChooser(new File("./exemples"));
		fC.setMultiSelectionEnabled(true);
		fC.setFileSelectionMode(JFileChooser.FILES_ONLY);
		fC.setFileFilter(new FileFilter() {
			@Override
			public String getDescription() {
				return "Fichier reseau (*.xml)";
			}

			@Override
			public boolean accept(File f) {
				return f.getName().endsWith(Parser.FILE_EXT);
			}
		});

		if (fC.showOpenDialog(interfacePrincipale) == JFileChooser.APPROVE_OPTION) {
			File copie = new File(workspace + File.separator + RES_DIR + File.separator + fC.getSelectedFile().getName());

			Parser.copyFile(fC.getSelectedFile(), copie);

			return copie;
		}

		return null;
	}

	/**
	 * Lance la procedure de selection du fichier de reseau, puis la phase de
	 * chargement du reseau. <br/> Si le reseau peut etre entierement charge il
	 * l'est, sinon le chargement partiel est choisi (cf. {@link Reseau})
	 * 
	 * @param cheminFichier
	 */
	public void choisirReseau(String cheminFichier) {

		try {
			parser.setFichier(cheminFichier);

			if (fullParsing()) {
				SwingWorker<Reseau, Void> wr = parser.getWorkerOnePass();
				wr.execute();
				reseau = wr.get();
			}
			else {

				JOptionPane.showMessageDialog(this.interfacePrincipale, "Le fichier ne peut etre complètement chargé en memoire. Il doit être segmenté.", "Attention", JOptionPane.INFORMATION_MESSAGE);

				File rootDir = parser.getFileSystemRoot(new File(cheminFichier).toURI().toURL());

				if (!rootDir.exists()) {

					SwingWorker<Void, Void> ws = parser.segmenteReseau();

					final JProgressBar progressBar = new JProgressBar(0, 100);
					progressBar.setPreferredSize(new Dimension(300, 30));
					progressBar.setStringPainted(true);
					progressBar.setIndeterminate(false);

					final JDialog dialog = new JDialog(this.interfacePrincipale, "Segmentation en cours...");
					dialog.setResizable(false);
					dialog.add(progressBar);
					dialog.pack();
					dialog.setLocationRelativeTo(this.interfacePrincipale);
					dialog.setAlwaysOnTop(true);
					
					dialog.setVisible(true);

					ws.addPropertyChangeListener(new PropertyChangeListener() {
						@Override
						public void propertyChange(PropertyChangeEvent evt) {
							if (evt.getPropertyName().equals("progress")) {
								int progress = (Integer) evt.getNewValue();

								progressBar.setValue(progress);

								if (progress == 100) {
									dialog.setVisible(false);
									dialog.dispose();
								}
							}

							if (evt.getPropertyName().equals(Parser.STATE_PROPERTY_NAME)) {
								progressBar.setString((String) evt.getNewValue());
							}
						}
					});
				}
				else {
					JOptionPane.showMessageDialog(this.interfacePrincipale, "Le fichier semble deja avoir �t� segment�. Si ce n'est pas le cas, veuillez supprimer le dossier " + rootDir.getAbsolutePath(), "Attention", JOptionPane.INFORMATION_MESSAGE);
					parser.setWorkspace(rootDir.getAbsolutePath());
					parser.segmentationDone(true);
				}

				SwingWorker<Hashtable<String, Ville>, Void> wv = parser.getWorkerListeVilles();
				SwingWorker<Hashtable<String, Route>, Void> wr = parser.getWorkerListeRoutes();

				wv.execute();
				wr.execute();

				reseau = new Reseau(this);
				setVilles(wv.get());
				setRoutes(wr.get());
			}

			reseau.setId(cheminFichier);
		}
		catch (Exception e) {
			e.printStackTrace();
		}
	}

	public Reseau getReseau() {
		return reseau;
	}

	public String getReseauID() {
		return reseau.getId();
	}

	public void setReseauID(String id) {
		reseau.setId(id);
	}

	public Hashtable<String, Route> getRoutes() {
		if(reseau != null){
			return reseau.getRoutes();
		}
		else{
			return new Hashtable<String, Route>();
		}
	}

	public void addRoute(Route route) {

		reseau.getRoutes().put(route.getNom(), route);

		if (!fullParsing()) {
			try {
				parser.addToFS(route);
			}
			catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

	public void removeRoute(Route route) {
		reseau.getRoutes().remove(route.getNom());

		if (!fullParsing()) {
			parser.removeFromFS(route);
		}
	}
	
	public void modifyRoute(String name, Route newRoute){
		if(!fullParsing()){
			parser.updateFS(name, newRoute);
		}
		
		reseau.getRoutes().remove(name);
		reseau.getRoutes().put(newRoute.getNom(), newRoute);
	}

	public void setRoutes(Hashtable<String, Route> routes) {
		reseau.setRoutes(routes);
	}

	public Hashtable<String, Ville> getVilles() {
		if(reseau != null){
			return reseau.getVilles();
		}
		else{
			return new Hashtable<String, Ville>();
		}
	}

	public void addVille(Ville ville) {

		reseau.getVilles().put(ville.getNom(), ville);

		if (!fullParsing()) {
			try {
				parser.addToFS(ville);
			}
			catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

	public void removeVille(Ville ville) {

		reseau.getVilles().remove(ville.getNom());

		if (!fullParsing()) {
			parser.removeFromFS(ville);
		}
	}
	
	public void modifyVille(String name, Ville newVille){
		if(!fullParsing()){
			parser.updateFS(name, newVille);
		}
		
		reseau.getVilles().remove(name);
		reseau.getVilles().put(newVille.getNom(), newVille);
	}

	public void setVilles(Hashtable<String, Ville> villes) {
		reseau.setVilles(villes);
	}

	public Ville getVilleDepart() {
		return preferences.getVilleDepart();
	}

	public void addTroncon(Troncon troncon) {
		if (!fullParsing()) {
			try {
				parser.addToFS(troncon);
			}
			catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

	public void removeTroncon(Troncon troncon) {
		if (!fullParsing()) {
			parser.removeFromFS(troncon);
		}

		troncon.disconnect();
	}
	
	public void modifyTroncon(Troncon troncon, Troncon newTroncon){
		if (!fullParsing()) {
			parser.updateFS(troncon, newTroncon);
		}

		troncon.disconnect();
	}

	public int getNbVilles() {
		if(reseau != null){
			return reseau.getVilles().size();
		}
		else{
			return 0;
		}
	}

	public int getNbRoutes() {
		if(reseau != null){
			return reseau.getRoutes().size();
		}
		else{
			return 0;
		}
	}

	public int getNbTroncons() {
		try {
			return getAllTroncons().size();
		}
		catch (Exception e) {
			return -1;
		}
	}

	public LinkedList<Troncon> getAllTroncons() throws Exception {
		LinkedList<Troncon> troncons = new LinkedList<Troncon>();

		for (Iterator<Entry<String, Route>> iterator = getRoutes().entrySet().iterator(); iterator.hasNext();) {

			Entry<String, Route> entry = iterator.next();

			troncons.addAll(getTroncons(entry.getValue()));
		}

		return troncons;
	}

	public LinkedList<Troncon> getTroncons(final Ville ville) throws Exception {
		if (fullParsing()) {
			return ville.getTroncons();
		}
		else {
			SwingWorker<LinkedList<Troncon>, Void> worker = parser.getWorkerTroncons(ville);
			worker.execute();

			return worker.get();
		}
	}

	public LinkedList<Troncon> getTroncons(final Route route) throws Exception {

		if (fullParsing()) {
			return route.getTroncons();
		}
		else {
			SwingWorker<LinkedList<Troncon>, Void> worker = parser.getWorkerTroncons(route);
			worker.execute();

			return worker.get();
		}
	}

	public Troncon getTroncon(Route route, Ville ville1, Ville ville2) throws Exception {

		for (Troncon troncon : getTroncons(route)) {
			if (troncon.getVille1().equals(ville1)) {
				if (troncon.getVille2().equals(ville2)) {
					return troncon;
				}
			}
			else {
				if (troncon.getVille2().equals(ville1)) {
					if (troncon.getVille1().equals(ville2)) {
						return troncon;
					}
				}
			}
		}

		return null;
	}

	public InterfacePrincipale getInterfacePrincipale() {
		return interfacePrincipale;
	}

	public PreferencesUtilisateur getPreferences() {
		return preferences;
	}

	public void setVilleDepart(Ville villeDepart) {
		preferences.setVilleDepart(villeDepart);
	}

	public Ville getVilleArrivee() {
		return preferences.getVilleArrivee();
	}

	public void setVilleArrivee(Ville villeArrivee) {
		preferences.setVilleArrivee(villeArrivee);
	}

	public CritereItineraire getCritereItineraire() {
		return preferences.getCritereItineraire();
	}

	public void setCritereItineraire(CritereItineraire critereItineraire) {
		preferences.setCritereItineraire(critereItineraire);
	}

	public CritereEtapes getEtapesObligatoires() {
		return preferences.getEtapesObligatoires();
	}

	public void addEtapeObligatoire(Ville ville) {
		preferences.addEtapeObligatoire(ville);
	}

	public CritereEtapes getEtapesInterdites() {
		return preferences.getEtapesInterdites();
	}

	public void addEtapeInterdite(Ville ville) {
		preferences.addEtapeInterdite(ville);
	}

	public LinkedList<CritereVille> getCriteresVilles() {
		return preferences.getCriteresVilles();
	}

	public void addCritereVille(CritereVille critereVille) {
		preferences.addCritereVille(critereVille);
	}

	public LinkedList<CritereTroncon> getCriteresTroncons() {
		return preferences.getCriteresTroncons();
	}

	public void addCritereTroncon(CritereTroncon critereTroncon) {
		preferences.addCritereTroncon(critereTroncon);
	}

	public ArrayList<Critere> getCriteres() {
		return preferences.getCriteres();
	}

	public void addCritere(Critere critere) {
		preferences.getCriteres().add(critere);
	}

	public Administration getAdministration() {
		return this.administration;
	}
	
	/**
	 * Verrouille/Deverouille les mises a jour
	 * @param lock <code>true</code> pour verouiller, <code>false</code> pour deverouiller
	 */
	public synchronized void lockUpdates(boolean lock){
		lockUpdates = lock;
		notifyAll();
	}
	
	/**
	 * Indicateur de verouillage des mises a jour
	 * @return <code>true</code> si les mises a jour son verouillee, <code>false</code> sinon
	 */
	public synchronized boolean updatesLocked(){
		return lockUpdates;
	}

	/**
	 * Lance la procedure de mise a jour. <br/> Appelle la fonction
	 * <code>maj()</code> de chaque objet {@link Modification} enregistre par
	 * le module {@link Administration}
	 */
	public synchronized void maj() {
		
		boolean showOnce = false;
		
		for (Modification modification : administration.getModifications()) {
			if(updatesLocked() && !calculateur.getItineraire().estAffectePar(modification)){
				modification.maj(this);
			}
			else{
				if(!showOnce){
					JOptionPane.showMessageDialog(this.interfacePrincipale, "Votre itinéraire a été affecté par des mises à jour du réseau. Un nouveau calcul est nécessaire", "Information", JOptionPane.INFORMATION_MESSAGE);
					showOnce = true;
					this.getInterfacePrincipale().getPanelNavigation().interrompreNavigation();
				}
				
			}
		}
		
		// Sauvegarde du fichier
		if(fullParsing()){
			new Thread(){
				@Override
				public void run() {
					try {
						
						File src = new File(new File(parser.getURLreseau().toURI()).getAbsolutePath() + Parser.TEMP_FILE_EXT);
						File dest = new File(new File(parser.getURLreseau().toURI()).getAbsolutePath());
						
						parser.ecrireReseau(src.getAbsolutePath());
						Parser.copyFile(src, dest);
					}
					catch (Exception e) {
						JOptionPane.showMessageDialog(Controleur.this.interfacePrincipale, "La sauvegarde du réseau à échouée", "Erreur", JOptionPane.ERROR_MESSAGE);
						e.printStackTrace();
					}
				}
			}.start();
		}
		
		// Deverouillage panel admin
		interfacePrincipale.getPanelAdministration().enableAll(true);
	}

	@Override
	public String toString() {
		return reseau.toString();
	}

	public Itineraire rectifieItineraire(Ville ville) {
		return calculateur.calculeReorientation(ville, this.preferences);
	}
}
